package gameframe;

import client.ClientPlayer;
import musicthread.MusicThread;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.LinkedList;

/**
 * @author Hsu琛君珩
 * @ClassName:ChessPanel
 * @date 2023-12-21
 * @apiNote
 * @Version: v1.0
 */

/**
 * 鼠标光标类。
 * 用于在游戏界面内绘制鼠标的信息，如光标位置和显示状态。
 */
class CursorTrigger {
    private int x; // 鼠标光标的X坐标
    private int y; // 鼠标光标的Y坐标
    private Image cursor; // 鼠标光标图像
    private boolean draw; // 是否绘制光标

    /**
     * 构造方法，初始化鼠标光标。
     */
    public CursorTrigger() {
        cursor = new ImageIcon("image/cursor.png").getImage(); // 加载光标图像
        draw = false; // 默认不显示光标
    }

    // Getter和Setter方法
    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x; // 更新光标的X坐标
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y; // 更新光标的Y坐标
    }

    public boolean isDraw() {
        return draw;
    }

    public void setDraw(boolean draw) {
        this.draw = draw; // 设置是否绘制光标
    }

    public Image getCursor() {
        return cursor; // 获取光标图像
    }
}

/**
 * 棋盘面板类。
 * 负责显示棋盘、棋子，并处理用户的交互。
 */
public class ChessPanel extends JPanel {

    /**
     * 单例模式的ChessPanel类，用于管理和显示五子棋游戏的棋盘。
     */
    private static ChessPanel instance; // 单例实例，确保整个应用中只有一个ChessPanel实例。

    // 棋盘的基准点和格子宽度设置，用于计算棋子放置的位置。
    private static final int BASEX = 42; // 棋盘的基准X坐标。
    private static final int BASEY = 42; // 棋盘的基准Y坐标。
    private static final int BLOCKWIDTH = 24; // 棋盘每个格子的宽度。

    // 图像资源，用于绘制棋子和棋盘背景。
    private static Image whiteChess; // 白棋的图像。
    private static Image blackChess; // 黑棋的图像。
    private static Image background; // 棋盘背景的图像。

    // 用于显示和处理鼠标光标的自定义类。
    private static CursorTrigger cursorTrigger; // 鼠标光标触发器，管理鼠标光标的显示。

    // 棋局状态的本地副本，用于绘制棋盘和棋子，以及管理复盘操作。
    private static LinkedList<Chess> chessStack = new LinkedList<>(); // 存储棋子的本地栈。
    private static String chatInfo; // 本地聊天信息副本，用于在棋盘旁显示聊天信息。
    private static int reshowIndex = 0; // 复盘操作的栈指针，指示复盘进行到哪一步。无需复盘时值为0。
    private static String currentPlayer = "未开始"; // 当前玩家信息，默认显示为"未开始"。


    public void setCurrentPlayer(String player) {
        this.currentPlayer = player;
        repaint(); // 重绘界面以更新显示的信息
    }

    /**
     * 获取ChessPanel的单例实例。
     * 如果实例不存在，则创建一个新实例。
     *
     * @return ChessPanel的单例实例
     */
    public static ChessPanel getInstance() {
        if (instance == null) {
            instance = new ChessPanel();
        }
        return instance;
    }

    /**
     * 构造方法，用于初始化棋盘面板。
     */
    public ChessPanel() {
        loadImages(); // 调用方法加载棋盘和棋子的图像资源。
        cursorTrigger = new CursorTrigger(); // 初始化鼠标光标触发器，用于管理鼠标在棋盘上的显示。

        initActionListener(); // 初始化鼠标事件监听器，处理用户的点击和鼠标移动事件。
        this.setBounds(0, 0, background.getWidth(null), background.getHeight(null)); // 设置棋盘面板的大小，与背景图片的大小一致。
    }

    /**
     * 加载图像资源。
     */
    private void loadImages() {
        whiteChess = new ImageIcon("image/white.png").getImage();
        blackChess = new ImageIcon("image/black.png").getImage();
        background = new ImageIcon("image/board.png").getImage();
    }

    /**
     * 初始化鼠标事件监听。
     * 处理用户的点击和鼠标移动事件。
     */
    private void initActionListener() {
        this.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                handleMouseClick(e);
            }
        });

        this.addMouseMotionListener(new MouseMotionListener() {
            @Override
            public void mouseDragged(MouseEvent e) {
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                handleMouseMove(e);
            }
        });
    }

    /**
     * 处理鼠标点击事件。
     * 当用户在棋盘上点击时，这个方法会尝试在相应位置放置棋子。
     *
     * @param e 鼠标事件对象，包含了点击的位置等信息。
     */
    private void handleMouseClick(MouseEvent e) {
        if (ClientPlayer.getInstance().isTurn()) { // 检查是否轮到当前玩家下棋。
            // 计算点击位置对应的棋盘坐标。
            int curX = (e.getX() - BASEX + BLOCKWIDTH / 2) / BLOCKWIDTH;
            int curY = (e.getY() - BASEY + BLOCKWIDTH / 2) / BLOCKWIDTH;
            // 检查计算出的坐标是否是一个有效的下棋位置。
            if (ClientPlayer.getInstance().checkIndex(curX, curY, 0)) {
                // 向服务器发送下棋请求，并更新本地棋子栈信息。
                chessStack = ClientPlayer.getInstance().putChess(ClientPlayer.getInstance().getChessColor(), curX, curY);
                MusicThread.getInstance().playChessSound(); // 播放下棋音效。
                repaint(); // 重绘棋盘以显示新下的棋子。
            } else {
                // 如果位置无效，显示对话框通知用户
                JOptionPane.showMessageDialog(this, "你会不会下棋啊！！！", "棋都不会下", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * 处理鼠标移动事件。
     * 当用户在棋盘上移动鼠标时，这个方法会更新鼠标光标的位置，并在棋盘上相应位置显示光标。
     *
     * @param e 鼠标事件对象，包含了鼠标的位置等信息。
     */
    private void handleMouseMove(MouseEvent e) {
        // 计算鼠标当前位置对应的棋盘坐标。
        int curX = (e.getX() - BASEX + BLOCKWIDTH / 2) / BLOCKWIDTH;
        int curY = (e.getY() - BASEY + BLOCKWIDTH / 2) / BLOCKWIDTH;
        // 检查坐标是否在棋盘范围内，且位置是否发生变化。
        if (curX >= 0 && curX <= 14 && curY >= 0 && curY <= 14 &&
                (curX != cursorTrigger.getX() || curY != cursorTrigger.getY())) {
            cursorTrigger.setX(curX); // 更新光标的X坐标。
            cursorTrigger.setY(curY); // 更新光标的Y坐标。
            cursorTrigger.setDraw(true); // 设置光标为绘制状态。
            repaint(); // 重绘棋盘以显示光标。
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        // 绘制棋盘背景
        g.drawImage(background, 0, 0, this);
        // 绘制鼠标光标（如果需要）
        drawCursor(g);
        // 绘制棋子
        drawChessPieces(g);
        // 新增绘制当前玩家信息的逻辑
        drawCurrentPlayerInfo(g);
        //绘制当前角色信息
        int chessColor = ClientPlayer.getInstance().getChessColor();
        String role = "未知";
        if (chessColor == Chess.WHITE) {
            role = "白棋";
        } else if (chessColor == Chess.BLACK) {
            role = "黑棋";
        } else {
            role = "观众";
        }

        drawPlayerRole(g, role);
    }

    /**
     * 绘制鼠标光标图像。
     * 当鼠标移动到棋盘上时，这个方法将根据鼠标的当前位置绘制一个光标图像。
     * 光标图像通常用于指示玩家可以在棋盘上放置棋子的位置。
     *
     * @param g 图形上下文对象，用于在组件上进行绘图。它提供了绘制文本、图形、图像的方法和属性。
     */
    private void drawCursor(Graphics g) {
        // 检查是否需要绘制光标（即鼠标是否在棋盘上）。
        if (cursorTrigger.isDraw()) {
            // 获取光标图像，并计算光标在棋盘上的绘制位置。
            // 位置计算考虑了棋盘的基准点(BASEX, BASEY)和每个格子的宽度(BLOCKWIDTH)。
            // 光标的位置应该位于鼠标指向的格子的中心。
            g.drawImage(
                    cursorTrigger.getCursor(),  // 光标图像
                    cursorTrigger.getX() * BLOCKWIDTH + BASEX - BLOCKWIDTH / 2,  // 计算光标的x坐标
                    cursorTrigger.getY() * BLOCKWIDTH + BASEY - BLOCKWIDTH / 2,  // 计算光标的y坐标
                    this  // 观察者对象，通常用于图像异步加载，这里传入组件自身即可
            );
        }
    }

    /**
     * 绘制棋子。
     * 在棋盘上根据当前的棋子栈绘制所有棋子。如果处于复盘模式，只绘制到复盘指定的步骤。
     *
     * @param g 图形上下文对象，用于在组件上进行绘图。
     */
    private void drawChessPieces(Graphics g) {
        // 安全检查，确保棋子栈不为空，如果为空则没有棋子需要绘制。
        if (chessStack == null) {
            return;
        }

        // 确定需要绘制的棋子范围。如果不处于复盘模式（reshowIndex为0），
        // 则绘制棋子栈中的所有棋子；如果处于复盘模式，则只绘制到复盘步骤指定的棋子。
        int drawUpToIndex = (reshowIndex == 0) ? chessStack.size() : reshowIndex;

        // 遍历棋子栈，绘制每个棋子。
        for (int i = 0; i < drawUpToIndex; i++) {
            // 获取当前要绘制的棋子。
            Chess c = chessStack.get(i);
            // 根据棋子颜色确定使用哪张图片（白棋或黑棋）。
            Image chessImage = (c.getColor() == Chess.WHITE) ? whiteChess : blackChess;
            // 计算棋子在棋盘上的绘制位置，并绘制棋子。
            g.drawImage(
                    chessImage,  // 使用的棋子图片
                    c.getPosX() * BLOCKWIDTH + BASEX - BLOCKWIDTH / 2,  // 计算棋子的x坐标
                    c.getPosY() * BLOCKWIDTH + BASEY - BLOCKWIDTH / 2,  // 计算棋子的y坐标
                    Chess.WIDTH,  // 棋子的宽度
                    Chess.HEIGHT,  // 棋子的高度
                    this  // 观察者对象
            );
            // 确定当前棋子是由哪方玩家放置，并更新当前轮到的玩家信息。
            String player = (c.getColor() == Chess.WHITE) ? "黑棋" : "白棋";
            setCurrentPlayer(player);
        }
    }

    /**
     * 绘制当前轮到的玩家信息。
     * 在棋盘上显示一个文本，告诉玩家当前轮到谁下棋。
     *
     * @param g 图形上下文对象
     */
    private void drawCurrentPlayerInfo(Graphics g) {
        g.setColor(Color.WHITE); // 设置文本颜色
        g.setFont(new Font("宋体", Font.BOLD, 18)); // 设置字体
        // 绘制文本
        g.drawString("当前轮到: " + currentPlayer, 20, 20);
    }

    /**
     * 绘制玩家的角色信息。
     * 在棋盘上显示玩家是白棋、黑棋还是观众。
     *
     * @param g    图形上下文对象
     * @param role 玩家的角色（白棋、黑棋或观众）
     */
    private void drawPlayerRole(Graphics g, String role) {
        g.setColor(Color.WHITE); // 设置文本颜色
        g.setFont(new Font("宋体", Font.BOLD, 18)); // 设置字体
        // 绘制文本
        g.drawString("你的角色: " + role, 200, 20);
    }

    /**
     * 更新本地的棋子栈和聊天信息。
     * 此方法在收到新的游戏状态时被调用，它更新当前组件维护的棋子位置和聊天记录，
     * 并触发重绘以反映新状态。
     *
     * @param chessStack 新的棋子栈，包含了所有棋子的当前位置。
     * @param chatInfo   新的聊天信息，包含了至目前为止的聊天记录。
     */
    public void update(LinkedList<Chess> chessStack, String chatInfo) {
        this.chessStack = chessStack;  // 更新棋子栈
        this.chatInfo = chatInfo;  // 更新聊天信息
        repaint();  // 触发重绘以更新界面
        ChatPanel.getInstance().updateText(chatInfo); // 同时更新聊天面板的信息
    }

    /**
     * 初始化复盘信息。
     * 设置复盘的初始状态，准备开始复盘。此方法通常在复盘开始前调用。
     */
    public void initRePlay() {
        this.reshowIndex = 0;  // 设置复盘的起始点为第一步
        repaint();  // 触发重绘以更新界面
    }

    /**
     * 复盘操作。
     * 逐步展示之前下过的棋子。每次调用这个方法都会在棋盘上显示下一步棋，
     * 直到所有棋子都被展示。一旦所有棋子都被展示，将通知客户端复盘结束。
     */
    public void reShow() {
        // 检查是否还有更多的棋子可以展示
        if (reshowIndex < chessStack.size()) {
            this.reshowIndex++;  // 移动到下一个棋子
            repaint();  // 触发重绘以更新界面
        } else {
            ClientPlayer.getInstance().finishReshow(); // 通知客户端复盘结束
        }
    }

}